package demo.service;

import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import io.lettuce.core.RedisClient;

//@Service
public class RedisLockService {

    private static Logger logger = LoggerFactory.getLogger(RedisLockService.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private ValueOperations<String, String> valueOperations;

    RedisClient redisClient;

    @PostConstruct
    public void init() {
        valueOperations = stringRedisTemplate.opsForValue();
        stringRedisTemplate.opsForValue().set("StanTest", "hello", 5, TimeUnit.SECONDS);
    }

    /**
     * 查询锁过期时间
     * 
     * @param lockName
     * @return
     *         <li>n>0,单位秒
     *         <li>-1：没设置超时
     *         <li>-2：KEY不存在
     *         <li>-3：异常
     */
    public long getExpire(String lockName) {
        if (lockName == null || lockName.isEmpty()) {
            throw new IllegalArgumentException("lockName can't be Empty");
        }
        try {
            return stringRedisTemplate.getExpire(lockName);
        } catch (Exception e) {
            logger.error("getExpire key {} failed", lockName, e);
            return -3;
        }
    }

    /**
     * <h1>锁超时是什么意思呢？</h1>如果一个得到锁的线程在执行任务的过程中挂掉，来不及显式地释放锁，这块资源将会永远被锁住，别的线程再也别想进来。
     * 所以，setnx的key必须设置一个超时时间，以保证即使没有被显式释放，这把锁也要在一定时间后自动释放。
     */
    public boolean tryLock(String lockName, long timeout, TimeUnit timeUnit) {
        if (lockName == null || lockName.isEmpty()) {
            throw new IllegalArgumentException("lockName can't be Empty");
        }
        try {
            if (timeout > 0 && timeUnit != null) {
                return valueOperations.setIfAbsent(lockName, "", timeout, timeUnit);
            } else {
                return valueOperations.setIfAbsent(lockName, "");
            }
        } catch (Exception e) {
            logger.error("delete key:{} error", lockName, e);
            return false;
        }
    }

    /**
     * 一个极端场景，假如某线程成功得到了锁，并且设置的超时时间是30秒。
     * 如果某些原因导致线程B执行的很慢很慢，过了30秒都没执行完，这时候锁过期自动释放，线程B得到了锁。
     * 随后，线程A执行完了任务，线程A接着执行del指令来释放锁。但这时候线程B还没执行完，线程A实际上删除的是线程B加的锁。
     * 
     * 怎么避免这种情况呢？可以在del释放锁之前做一个判断，验证当前的锁是不是自己加的锁。
     * 
     * 至于具体的实现，可以在加锁的时候把当前的线程ID当做value，并在删除之前验证key对应的value是不是自己线程的ID。
     */
    public boolean releaseLock(String lockName) {
        if (lockName == null || lockName.isEmpty()) {
            throw new IllegalArgumentException("lockName can't be Empty");
        }
        try {
            stringRedisTemplate.delete(lockName);
        } catch (Exception e) {
            logger.error("delete key:{} error", lockName, e);
            return false;
        }
        return true;
    }

    /**
     * 解当前线程加的锁。
     * 
     * 虽然我们避免了线程A误删掉key的情况，但是同一时间有A，B两个线程在访问代码块，仍然是不完美的。
     * 
     * 怎么办呢？我们可以让获得锁的线程开启一个守护线程，用来给快要过期的锁“续航”。
     * 
     * 当过去了29秒，线程A还没执行完，这时候守护线程会执行expire指令，为这把锁“续命”20秒。守护线程从第29秒开始执行，每20秒执行一次。
     * 
     * 当线程A执行完任务，会显式关掉守护线程。
     * 
     * 另一种情况，如果节点1 忽然断电，由于线程A和守护线程在同一个进程，守护线程也会停下。这把锁到了超时的时候，没人给它续命，也就自动释放了。
     * 
     * @return
     */
    public boolean unLockCurrent(String key) {
        long threadId = Thread.currentThread().getId();

        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//        redisClient.eval(luaScript, Collections.singletonList(key),
//                Collections.singletonList(Thread.currentThread().getId()));
        return true;

    }

}
